Skip to content

Warn when companion extensions are too old for inline-script envs - #1777

Merged
Stella Huang (StellaHuang95) merged 1 commit into
microsoft:mainfrom
StellaHuang95:stellahuang/inline-script-extension-version-warning
Sep 11, 2026
Merged

Warn when companion extensions are too old for inline-script envs#1777
Stella Huang (StellaHuang95) merged 1 commit into
microsoft:mainfrom
StellaHuang95:stellahuang/inline-script-extension-version-warning

Conversation

@StellaHuang95

Copy link
Copy Markdown
Contributor

Problem

A PEP 723 inline-script environment is built correctly by this extension on its own, but it only reaches the language server once two companion changes ship:

Until both are present, setup reports success and the user is left without the full inline script experience, with nothing explaining why.

Change

Warn once, after a successful setup rather than before one. That is the moment the gap becomes visible, and the creation progress notification is already gone — prompting up front would be noise stacked on top of it.

Both entry points prompt at the handler level (setupInlineScriptEnvironmentHandler and setUpInlineScriptEnvironmentsInWorkspace), so a bulk run over N scripts shows one notification rather than N.

The prompt offers Update Extension and Don't Show Again; the latter persists in global state. Dismissing without choosing an action suppresses it for the session only.

Version handling

stable pre-release
Python 2026.4.0 2026.7.2026082601
Pylance 2026.3.1 2026.3.101

Values are the newest version on each channel that still lacks the change; anything newer passes.

Notes on why this is more involved than a single >=:

  • Per channel, because the two lines interleave numerically. Pre-release 2026.5.x/2026.7.x sort above stable 2026.4.0, so one threshold cannot express both — a future stable such as 2026.6.0 would be wrongly rejected.
  • Channel is inferred from the patch component. VS Code does not populate __metadata.preRelease for installed extensions (verified empty on every installed extension locally, including a known pre-release build). Python pre-releases carry a build-date patch, Pylance pre-releases use a 100+ counter. Both rules were checked against every published version above the relevant floors.
  • PEP 440 ordering, since these extensions are not semver.
  • Dev builds are skipped, not compared. 2026.7.0-dev is valid PEP 440 and normalizes to 2026.7.0.dev0, which sorts below every real build of the same minor — comparing it would flag anyone running a local build of either extension.
  • A missing extension is not outdated. Pylance is optional.
  • Pylance is skipped unless python.languageServer selects it, so Jedi and None users are not asked to update an extension they do not use.

Each outdated combination has its own complete l10n string rather than a joined extension list, since conjunctions and word order are locale-specific.

Follow-up

The thresholds are a snapshot of the newest version lacking the change. If either extension ships another release before the change lands, the corresponding constant must be bumped, or the warning silently stops firing for users on that release.

Once the real shipping versions are known, consider inverting to "first version with the change" (< instead of <=): that framing fails toward an over-eager prompt, which is visible and reported, rather than toward silence.

Testing

36 new unit tests in src/test/features/inlineScript/extensionVersionCheck.unit.test.ts, covering both channels for both extensions, the dev-build case, the language-server gate, and all dismissal paths.

Full unit suite: 2047 passing, 0 failing (clean out/ rebuild). tsc --noEmit, eslint src, and prettier --check all clean.

A PEP 723 inline-script environment is built correctly by this extension
alone, but it only reaches the language server once ms-python.python
resolves interpreters per file (exactResource, PR #26129) and Pylance
handles the file-scoped python/didChangeFilePythonPath notification
(PR #9302). Without both, setup reports success and the user is left
without the full inline script experience and no explanation.

Warn once, after a successful setup rather than before one, since that is
the moment the gap becomes visible and the creation progress notification
is already gone. Both the CodeLens command and the bulk command prompt at
the handler level, so a bulk run shows one notification rather than one
per script.

The prompt offers "Update Extension" and "Don't Show Again"; the latter
persists in global state. Dismissing the notification without choosing an
action suppresses it for the session only.

Version handling notes:

- Thresholds are per channel because the two lines interleave
  numerically: pre-release 2026.5.x/2026.7.x sort above stable 2026.4.0,
  so a single threshold cannot express both. Channel is inferred from the
  patch component, since VS Code does not populate
  __metadata.preRelease for installed extensions. Both inference rules
  were checked against every published version above the relevant floors.
- PEP 440 ordering is used, as these extensions are not semver.
- Dev builds are skipped rather than compared. "2026.7.0-dev" is valid
  PEP 440 and normalizes to 2026.7.0.dev0, which sorts below every real
  build of the same minor, so comparing it would flag anyone running a
  local build.
- A missing extension is not treated as outdated; Pylance is optional.
- Pylance is skipped unless python.languageServer selects it, so Jedi and
  None users are not asked to update an unused extension.

Each outdated combination has its own complete l10n string rather than a
joined extension list, since conjunctions and word order are
locale-specific.

The thresholds record the newest version on each channel that lacks the
required change, and must be refreshed if either extension ships another
release before the change lands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rchiodo

Rich Chiodo (rchiodo) commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

notifyInlineScriptSetupOutcome(uri, routing);
return;
}
await promptUpdateExtensionsForInlineScripts();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

If the advisory prompt rejects after environment creation, this handler reports setup as failed. Isolate prompt failures from the setup try block, matching the bulk path, and add a rejection test.

Common.dontShowAgain,
);

if (result === InlineScriptStrings.updateExtension) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

When both extensions are outdated, the combined prompt names both but outdated[0] opens only Python, and the session latch prevents another prompt. Provide separate actions or make every named update reachable.

@rchiodo

Copy link
Copy Markdown
Contributor

Result: 🔴 could-not-verify

Verification details

Verification: Isolated verification observed failures that were not classified as caused by this PR: Offline npm dependency bootstrap. The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Summary: No meaningful tests could run because the container lacked Node.js, npm, and `node_modules`; `npm ci --offline` failed with `npm: not found`. The two targeted inline-script unit suites were therefore not run. I found 37 newly added tests covering version classification, thresholds, language-server gating, and prompt actions. Explicit coverage is still missing for invoking the prompt from the single-file and bulk setup entry points.

Test runs: 1 passed, 1 failed, 2 not run

  • ⚠️ Not run | Inline-script extension version check unit suite | npm run compile-tests && node ./node_modules/mocha/bin/mocha.js --require source-map-support/register --require out/test/unittests.js --ui tdd --timeout 180000 out/test/features/inlineScript/extensionVersionCheck.unit.test.js
  • ⚠️ Not run | Inline-script setup environment regression suite | npm run compile-tests && node ./node_modules/mocha/bin/mocha.js --require source-map-support/register --require out/test/unittests.js --ui tdd --timeout 180000 out/test/features/inlineScript/setupEnvironment.unit.test.js
  • Failed | unrelated to this PR | Offline npm dependency bootstrap | npm ci --offline
  • Passed | Dependency and targeted-test discovery | printf '%s\n' '=== changed files ==='; git diff --name-status upstream/main...HEAD; printf '%s\n' '=== relevant package scripts ==='; node -e "const p=require('./package.json'); for (const [k,v] of Object.entries(p.scripts||{})) if (/test|compile|lint|prettier/.test(k)) console.log(k+'='+v)"; printf '%s\n' '=== dependency and build state ==='; if [ -d node_modules ]; then echo 'node_modules=present'; else echo 'node_modules=missing'; fi; if [ -x node_modules/.bin/jest ]; then echo 'jest=present'; else echo 'jest=missing'; fi; if [ -x node_modules/.bin/mocha ]; then echo 'mocha=present'; else echo 'mocha=missing'; fi; if [ -d out ]; then echo 'out=present'; else echo 'out=missing'; fi; printf '%s\n' '=== targeted test files ==='; find src/test/features/inlineScript -maxdepth 1 -type f -name '*test.ts' -print | sort
⚠️ Inline-script extension version check unit suite diagnostic output
Not run because the offline dependency bootstrap failed: /bin/sh: npm: not found.
⚠️ Inline-script setup environment regression suite diagnostic output
Not run because the offline dependency bootstrap failed: /bin/sh: npm: not found.
Offline npm dependency bootstrap diagnostic output
/bin/sh: 1: npm: not found

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) added the review-auto:approved Automated review: no blocking findings (approval posted). label Sep 10, 2026
@StellaHuang95
Stella Huang (StellaHuang95) merged commit db0a3b2 into microsoft:main Sep 11, 2026
47 of 48 checks passed
@StellaHuang95
Stella Huang (StellaHuang95) deleted the stellahuang/inline-script-extension-version-warning branch September 11, 2026 00:14
Stella Huang (StellaHuang95) added a commit that referenced this pull request Sep 11, 2026
…nments (#1780)

> Part of #1602. Design: #1601. Follow-up to #1772.

## Summary

Fix PEP 723 inline-script cache and routing issues, make cached
environments read-only through the package-management UI, and support
deleting one cached environment without clearing the entire script
cache.

The feature remains behind the internal, undeclared
`python-envs.inlineScripts.enabled` flag, which defaults to `false` and
is latched at activation. This does not enable inline scripts by default
or add automatic installation on Run/Debug.

## Commits and user-visible changes

| Commit | Problem or missing behavior | Change |
|---|---|---|
| `64960273` | The first package enumeration could be mistaken for an
installation, so expanding Packages or refreshing inventory invalidated
a working script environment. Case-only Windows renames could also lose
the association. | Replace package-event-based drift detection with a
recorded inventory hash; hide package mutation actions and reject the
palette install path for inline environments; preserve Windows case-only
rename associations, including dirty aliases and pending reads. |
| `669ffe18` | A lookup could detect drift and return no environment
while routing still advertised the cache as valid and the setup CodeLens
remained hidden. Other scripts sharing the entry could retain stale
state. | Confirm drift against the current entry, invalidate affected
routing/cached validation, and notify environment consumers without
requiring a save. Protect newer repairs and selections from stale
observations. |
| `1d3ea92a` | Delete Environment was offered in the Projects view, but
the inline manager did not implement removal and returned "Remove
Environment not supported". | Delete the selected cache entry without a
confirmation dialog, clear its known workspace associations, and update
the environment collection. Preserve script files, project
entries/settings, the base interpreter, and unrelated cache entries. |

## Implementation and behavior notes

### Record package inventory instead of interpreting package events

- When inventory is readable, setup records an `installedPackagesHash`
in the entry's `.meta.json`, under the existing cache-entry lock. The
hash is computed from sorted, case-normalized `*.dist-info` names, which
include distribution names and versions.
- Validation and reuse compare the recorded inventory with disk. Merely
expanding or refreshing the package list no longer has authority to
invalidate the environment.
- A missing baseline or unreadable inventory is **unknown**, not
evidence that packages were added. Existing `manuallyModified` sidecars
remain honored.
- The package list stays visible, but inline environments do not offer
Install, Uninstall, or Change Version actions in the trees. The palette
install command explains that dependencies should be edited in the
script's metadata and setup rerun.
- No new package-provider event metadata, per-package operation queues,
or provider opt-in interfaces are introduced. The pip, Conda, Poetry,
shared package-change helper, and package-watcher implementations are
unchanged.

### Keep routing and the setup action consistent

- A confirmed mismatch clears the affected scripts' cached environments
and validation state, advances revisions, restores their setup
CodeLenses, and publishes environment changes.
- Invalidation rechecks the current sidecar/inventory under a
non-waiting cache-entry lock. An observation made before a repair must
not invalidate the repaired entry.
- Busy entries and unavailable confirmation reads do not publish stale
invalidations. A newer selection wins when an older lookup finishes
later.
- Detection is on demand, after the existing five-second validation
cache expires; this is not a new background polling service.

### Delete one cached environment

- Delete means physical deletion whether the entry has one associated
script or several. It is not silently changed into "detach this script"
for shared entries.
- The action has **no confirmation dialog**. Scripts sharing the deleted
entry need setup again.
- Reuse the existing cache maintenance, ownership/path checks, locks,
and association-cleanup machinery, with a single-entry target. Do not
invoke the bulk project-settings cleanup.
- Route an inline Projects-view item through the manager owning the
clicked environment, rather than the file's potentially changed fallback
manager. Ordinary environment removal keeps its existing routing.
- Reject missing arguments, foreign/out-of-cache targets, redirected
entries, and active creation conflicts. Preserve unrelated and
unrecognized association records.
- Invalidate a valid sidecar before filesystem removal so an interrupted
deletion does not leave surviving cache files trusted as healthy. Report
filesystem/persistence failures and update local state for what was
actually removed.

## Testing

Local validation before the rebase/signing update (`1afe4bc9`):

- `npm run compile-tests`: passed.
- `npm run compile` (webpack extension bundle): passed.
- `npm run lint`: passed.
- Full unit suite: **2,074 passing, 6 pending**.

Coverage includes get-only drift detection and CodeLens recovery, shared
versus unrelated entries, stale observations after newer
repairs/selections, real cache-entry locks, unavailable inventory,
Windows case-only renames, palette restrictions, targeted deletion
without prompting, protected source/settings/base-Python files,
already-missing entries, unsafe paths, partial failures, and concurrent
create/delete/rehydration.

### Suggested manual verification

These are reviewer checks, not a claim that a full live VS Code
end-to-end pass has been completed:

1. Enable `python-envs.inlineScripts.enabled` and reload. Use the Python
Environments integration and companion builds that support per-file
interpreters.
2. Set up two scripts with the same metadata so they share a cached
environment, plus a third script using a different cache entry.
3. Expand/refresh Packages: setup remains valid and inline package
mutation actions are absent. The palette install action for an inline
project should show guidance rather than install.
4. In a disposable test environment, change installed packages
externally. After the validation cache expires, request the script's
environment without saving. Verify the affected setup actions return and
explicit setup repairs the entry.
5. On Windows, rename `job.py` to `Job.py`; verify the existing
environment remains associated and unsaved metadata is not replaced with
stale disk metadata.
6. Delete a shared inline environment from the Projects view. Verify
there is no confirmation, only that cache entry is deleted, affected
scripts need setup again, and the unrelated environment, scripts,
project settings, and base Python remain.
7. Exercise a busy/failed deletion and verify it reports failure rather
than success. Recheck normal package management/removal for an ordinary
venv.

## Scope and known follow-ups

- Inventory comparison is not a full package-integrity audit: it does
not inspect package contents or legacy `.egg-info`-only installations.
- Shared cache entries can be referenced by other workspaces.
Association cleanup/notifications here are local to the current
workspace; other windows notice deletion on revalidation. Cache locks do
not track arbitrary running Python processes, so running jobs should be
stopped before deletion.
- The current unknown-inventory behavior is retained. The case where an
entirely missing `site-packages` directory is accepted during reuse
remains a follow-up; deleting the cache entry and running setup provides
a recovery path.
- A rejected inline selection can still briefly show the temporary
"selected" badge; the actual selection is not changed. That feedback fix
is separate from the routing invalidation addressed here.
- Companion-version guidance is covered separately by #1777. No
installer-backend, retention-policy, or Run/Debug auto-setup changes are
included.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a4277008-9dd3-4beb-b9ac-4197eb085404
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants